package qscript;

import g4p_controls.G4P;
import g4p_controls.GAlign;
import g4p_controls.GButton;
import g4p_controls.GCScheme;
import g4p_controls.GCheckbox;
import g4p_controls.GCustomSlider;
import g4p_controls.GEvent;
import g4p_controls.GLabel;
import g4p_controls.GTextArea;

import java.awt.Font;
import java.util.LinkedList;
import java.util.TreeMap;

import org.qscript.Argument;
import org.qscript.Script;
import org.qscript.Solver$;
import org.qscript.Variable;
import org.qscript.errors.ErrorType;
import org.qscript.events.EvaluationErrorEvent;
import org.qscript.events.HaltExecutionEvent;
import org.qscript.events.OutputEvent;
import org.qscript.events.ScriptEvent;
import org.qscript.events.ScriptFinishedEvent;
import org.qscript.events.StoreUpdateEvent;
import org.qscript.events.SyntaxErrorEvent;
import org.qscript.events.TraceEvent;
import org.qscript.events.WaitEvent;
import org.qscript.eventsonfire.EventHandler;

import processing.core.PApplet;
import processing.data.StringList;

public class ScriptEditor extends PApplet {

	/**
	 <h1>QScript algorithm editor</h1>
	 <p>There are 3 predefined scripts demonstrating solutions to simple math 
	 algorithms. To execute one of these scripts click on one of the green 
	 buttons, then click on the Init button, this will parse and tokenise 
	 the script. </p>

	 <p>In the event of a syntax error it will be highlighted in red and a 
	 description will appear in the pink status bar. </p>

	 <p>It is not possible to run the script until it has been successfully 
	 tokenised. At that time you can click on the start button, slect whether you 
	 want to trace the progress of the evaluation and if tracing the speed of 
	 evaluation.</p>

	 <p>To run your own scripts simply type them into the script editor and then
	 initialise and run them.</p>

	 <p>You can see all the operators/functions available  
	 <a href="http://www.lagers.org.uk/qscript/operatorset.html" target="_blank">here</a>. </p>
	 <br/><br/>

	 */


	final int EDIT = 1;
	final int RUNNING = 3;

	int currMode;

	Script script;
	String code[];
	boolean codeChanged = false;
	TreeMap<String, String> store = new TreeMap<String, String>();
	LinkedList<ScriptEvent> event_queue = new LinkedList<ScriptEvent>();

	StringBuilder output = new StringBuilder();

	public void setup() {
		size(600, 570, JAVA2D);
		createGUI();
		customGUI();
		// Set the script to start with
		script = new Script("");
		setScript(1);
		// Set the script trace and trace delays based on initial GUI settings
		script.traceDelay(sdrDelay.getValueI());
		setTraceMode(cbxTrace.isSelected());

		// Scripting events are to be sent to this object
		script.addListener(this);

		goToMode(EDIT);
		registerMethod("pre", this);
	}

	// Switch IDE between running and editing modes
	public void goToMode(int mode) {
		switch(mode) {
		case EDIT:
			currMode = mode;
			btnStart.setAlpha(255);
			btnStop.setAlpha(120);
			break;
		case RUNNING:
			currMode = mode;
			btnStart.setAlpha(120);
			btnStop.setAlpha(255);
			break;
		}
	}  


	// Clear the data store of all variables
	public void clearVars() {
		store.clear();
		txaVars.setText("");
		if (script != null)
			script.clearVariables();
	}

	// Hide/show trace controls depending on whether the
	// trace is switched on or off.
	public void setTraceMode(boolean traceOn) {
		if (script != null) {
			if(traceOn){
				lblDelayTitle.setVisible(true);
				sdrDelay.setVisible(true);
				btnPause.setVisible(true);
				btnPause.setAlpha(255);
				btnResume.setVisible(true);
				btnResume.setAlpha(120);
				script.traceModeOn();
			}
			else {
				lblDelayTitle.setVisible(false);
				sdrDelay.setVisible(false);
				btnPause.setVisible(false);
				btnPause.setAlpha(255);
				btnResume.setVisible(false);
				btnResume.setAlpha(120);
				script.traceModeOff();
				script.resume();
				txaScript.clearStyles();
			}
		}
	}

	/**
	 * Since events are fired asynchronously this method might be called whilst G4P
	 * is drawing its controls causing the program to crash. To avoid this we will
	 * add the event to a queue and process it during Processing's event loop.
	 */
	@EventHandler
	public void onScriptEvent(ScriptEvent event) {
		if (event instanceof HaltExecutionEvent && event.etype == ErrorType.STOPPED_BY_USER)
			event_queue.addFirst(event);
		else
			event_queue.addLast(event);
	}

	/**
	 * This method has been registered with Processing so will be called just 
	 * before the draw method. It will process a maximum of 20 events in the FIFO
	 * queue, then allow the draw method to execute.
	 * Since the script can generate hundreds of events per frame we have to
	 * cap the number processed if we want the GUI to be responsive.
	 */
	public void pre() {
		int count = 0;
		while (!event_queue.isEmpty () && count++ < 20)
			performEventAction(event_queue.removeFirst());
	}

	/**
	 * This method performs the actions needed by any particular type of script 
	 * event. 
	 */
	public void performEventAction(ScriptEvent event) {
		if (event instanceof TraceEvent && cbxTrace.isSelected()) {
			txaScript.clearStyles();
			txaScript.addStyle(G4P.BACKGROUND, 0xFF0000CC, event.lineNo, event.pos, event.pos + event.width);
			txaScript.addStyle(G4P.FOREGROUND, 0xFFFFFF00, event.lineNo, event.pos, event.pos + event.width);
			txaScript.moveCaretTo(event.lineNo, event.pos);
		} else if (event instanceof SyntaxErrorEvent || event instanceof EvaluationErrorEvent) {
			lblStatus.setText(event.getMessage());
			txaScript.clearStyles();
			txaScript.addStyle(G4P.BACKGROUND, 0xFFFF0000, event.lineNo, event.pos, event.pos + event.width);
			txaScript.addStyle(G4P.FOREGROUND, 0xFFFFFFFF, event.lineNo, event.pos, event.pos + event.width);
			txaScript.addStyle(G4P.WEIGHT, 4, event.lineNo, event.pos, event.pos + event.width);
			txaScript.moveCaretTo(event.lineNo, event.pos);
			goToMode(EDIT);
		} else if (event instanceof HaltExecutionEvent) {
			lblStatus.setText(event.getMessage());
			event_queue.clear();
			txaScript.clearStyles();
			txaScript.addStyle(G4P.BACKGROUND, 0xFFFF0000, event.lineNo, event.pos, event.pos + event.width);
			txaScript.addStyle(G4P.FOREGROUND, 0xFFFFFFFF, event.lineNo, event.pos, event.pos + event.width);
			txaScript.addStyle(G4P.WEIGHT, 4, event.lineNo, event.pos, event.pos + event.width);
			goToMode(EDIT);
		} else if (event instanceof ScriptFinishedEvent) {
			lblStatus.setText(event.getMessage());
			event_queue.clear();
			txaScript.clearStyles();
			goToMode(EDIT);
		} else if (event instanceof OutputEvent) {
			output.append(event.getMessage().toString());      
			txaOutput.setText(output.toString(), 2000);
		} else if (event instanceof StoreUpdateEvent) {
			Variable var = (Variable) event.extra[0];
			store.put(var.getIdentifier(), var.toString());
			StringBuilder s = new StringBuilder();
			for (String id : store.keySet ()) {
				if (id.length() > 12) {
					s.append(id.substring(0, 9));
					s.append("...   ");
				} else {
					s.append(id);
					s.append("               ".substring(0, 15 - id.length()));
				}
				s.append(store.get(id));
				s.append("\n");
			}
			txaVars.setText(s.toString());
		} else if (event instanceof WaitEvent) {
			Argument arg = (Argument) event.extra[0];
			int time = arg.toInteger();
			if (time == 0)
				lblStatus.setText("Waiting for you to resume ... ");
			else
				lblStatus.setText("Waiting for " + time + " milliseconds");
		}
	}

	public void draw() {
		background(230, 230, 255);
	}

	// Use this method to add additional statements
	// to customise the GUI controls
	public void customGUI() {
		//  lblStatus.setTextAlign(GAlign.LEFT, null);
		txaVars.setFont(new Font("Monospaced", Font.PLAIN, 11));
		txaOutput.setFont(new Font("Dialog", Font.PLAIN, 11));
		txaScript.setFont(new Font("Monospaced", Font.PLAIN, 12));
		txaVars.setTextEditEnabled(false);
		txaOutput.setTextEditEnabled(false);
	}


	//========================================================================================================================

	public void setScript(int sn) {
		String fname = null;
		StringList codeLines = new StringList();
		String desc = "";
		switch(sn) {
		case 1:
			fname = "quadratic.txt";
			break;
		case 2:
			fname = "fibonacci.txt";
			break;
		case 3:
			fname = "primes.txt";
			break;
		}
		if (fname == null)
			return;
		String[] lines = loadStrings("data/qscript/" + fname);
		int ln = 0;
		while (ln < lines.length) {
			if (lines[ln].startsWith("##Desc")) {
				ln++;
				while (!lines[ln].startsWith ("##") && ln <lines.length)
					desc += lines[ln++] + " ";
			}
			if (lines[ln].startsWith("##Code")) {
				ln++;
				while (!lines[ln].startsWith ("##") && ln <lines.length)
					codeLines.append(lines[ln++]);
			}
			ln++;
		}
		code = codeLines.array();
		script.setCode(code);
		txaScript.setText(code);
		txaOutput.setText(desc, 240);
		txaVars.setText("");
		lblStatus.setText("");
		codeChanged = true;
		goToMode(EDIT);
	}


	public void exit(){
		System.out.println("Exiting");
		Solver$.shutDown();
	}

	//========================================================================================================================

	// GUI tab
	public void script_change(GTextArea source, GEvent event) {
		if (event == GEvent.CHANGED) {
			txaOutput.setText("");
			lblStatus.setText("");
			txaScript.clearStyles();
			codeChanged = true;
		}
	}

	public void delay_change(GCustomSlider source, GEvent event) {
		if (script != null) {
			script.traceDelay(source.getValueI());
		}
	}

	public void start_click(GButton source, GEvent event) {
		goToMode(RUNNING);
		txaScript.clearStyles();
		if (codeChanged) {
			script.setCode(txaScript.getTextAsArray());
			codeChanged = false;
		}
		lblStatus.setText("");
		output = new StringBuilder();
		setTraceMode(cbxTrace.isSelected());
		txaOutput.setText("");
		clearVars();
		Solver$.evaluate(script);
	}

	public void stop_click(GButton source, GEvent event) {
		script.stop();
	}

	public void pause_click(GButton source, GEvent event) {
		script.waitFor(0);
		source.setAlpha(120);
		btnResume.setAlpha(255);
	}

	public void resume_click(GButton source, GEvent event) {
		script.resume();
		source.setAlpha(120);
		btnPause.setAlpha(255);
	}

	public void trace_click(GCheckbox source, GEvent event) {
		setTraceMode(source.isSelected());
	}

	public void script1_click(GButton source, GEvent event) {
		if (currMode != RUNNING)
			setScript(1);
	}

	public void script2_click(GButton source, GEvent event) {
		if (currMode != RUNNING)
			setScript(2);
	}

	public void script3_click(GButton source, GEvent event) {
		if (currMode != RUNNING)
			setScript(3);
	}

	// Create all the GUI controls. 
	public void createGUI() {
		G4P.messagesEnabled(false);
		G4P.setGlobalColorScheme(GCScheme.BLUE_SCHEME);
		G4P.setCursor(ARROW);
		if (frame != null)
			frame.setTitle("Sketch Window");
		txaScript = new GTextArea(this, 0, 20, 350, 450, G4P.SCROLLBARS_BOTH);
		txaScript.setPromptText("Enter your script");
		txaScript.setOpaque(true);
		txaScript.addEventHandler(this, "script_change");
		txaVars = new GTextArea(this, 350, 20, 250, 210, G4P.SCROLLBARS_BOTH);
		txaVars.setOpaque(true);
		txaOutput = new GTextArea(this, 350, 250, 250, 220, G4P.SCROLLBARS_BOTH);
		txaOutput.setLocalColorScheme(GCScheme.GREEN_SCHEME);
		txaOutput.setOpaque(false);
		lblScriptTitle = new GLabel(this, 0, 0, 350, 20);
		lblScriptTitle.setText("QScript editor");
		lblScriptTitle.setTextBold();
		lblScriptTitle.setOpaque(true);
		lblOutputTitle = new GLabel(this, 350, 230, 250, 20);
		lblOutputTitle.setText("Output");
		lblOutputTitle.setTextBold();
		lblOutputTitle.setLocalColorScheme(GCScheme.GREEN_SCHEME);
		lblOutputTitle.setOpaque(true);
		lblVariablesTitle = new GLabel(this, 350, 0, 250, 20);
		lblVariablesTitle.setText("Variables");
		lblVariablesTitle.setTextBold();
		lblVariablesTitle.setOpaque(true);
		lblStatusTitle = new GLabel(this, 10, 500, 70, 20);
		lblStatusTitle.setText("Status:");
		lblStatusTitle.setTextBold();
		lblStatusTitle.setLocalColorScheme(GCScheme.RED_SCHEME);
		lblStatusTitle.setOpaque(true);
		lblStatus = new GLabel(this, 80, 500, 510, 20);
		lblStatus.setTextAlign(GAlign.LEFT, GAlign.MIDDLE);
		lblStatus.setText(" ");
		lblStatus.setTextBold();
		lblStatus.setTextItalic();
		lblStatus.setLocalColorScheme(GCScheme.RED_SCHEME);
		lblStatus.setOpaque(true);
		sdrDelay = new GCustomSlider(this, 250, 470, 200, 20, "grey_blue");
		sdrDelay.setLimits(50.0f, 0.0f, 500.0f);
		sdrDelay.setEasing(10.0f);
		sdrDelay.setNumberFormat(G4P.INTEGER, 0);
		sdrDelay.setOpaque(true);
		sdrDelay.addEventHandler(this, "delay_change");
		btnStart = new GButton(this, 10, 470, 50, 20);
		btnStart.setText("Start");
		btnStart.setTextBold();
		btnStart.addEventHandler(this, "start_click");
		btnStop = new GButton(this, 70, 470, 50, 20);
		btnStop.setText("Stop");
		btnStop.setTextBold();
		btnStop.addEventHandler(this, "stop_click");

		btnPause = new GButton(this, 460, 470, 60, 20);
		btnPause.setText("Pause");
		btnPause.setTextBold();
		btnPause.addEventHandler(this, "pause_click");

		btnResume = new GButton(this, 530, 470, 60, 20);
		btnResume.setText("Resume");
		btnResume.setTextBold();
		btnResume.addEventHandler(this, "resume_click");

		cbxTrace = new GCheckbox(this, 130, 470, 70, 20);
		cbxTrace.setIconAlign(GAlign.RIGHT, GAlign.MIDDLE);
		cbxTrace.setText("Trace");
		cbxTrace.setTextBold();
		cbxTrace.setOpaque(true);
		cbxTrace.addEventHandler(this, "trace_click");
		lblDelayTitle = new GLabel(this, 210, 470, 50, 20);
		lblDelayTitle.setText("Delay");
		lblDelayTitle.setTextBold();
		lblDelayTitle.setOpaque(true);
		btnScript1 = new GButton(this, 10, 530, 180, 30);
		btnScript1.setText("Solve Quadratic Equation");
		btnScript1.setTextBold();
		btnScript1.setLocalColorScheme(GCScheme.ORANGE_SCHEME);
		btnScript1.addEventHandler(this, "script1_click");
		btnScript2 = new GButton(this, 210, 530, 180, 30);
		btnScript2.setText("Fibonacci Series");
		btnScript2.setTextBold();
		btnScript2.setLocalColorScheme(GCScheme.ORANGE_SCHEME);
		btnScript2.addEventHandler(this, "script2_click");
		btnScript3 = new GButton(this, 410, 530, 180, 30);
		btnScript3.setText("Prime Number Sieve");
		btnScript3.setTextBold();
		btnScript3.setLocalColorScheme(GCScheme.ORANGE_SCHEME);
		btnScript3.addEventHandler(this, "script3_click");
	}

	// Variable declarations 

	GTextArea txaScript; 
	GTextArea txaVars; 
	GTextArea txaOutput; 
	GLabel lblScriptTitle; 
	GLabel lblOutputTitle; 
	GLabel lblVariablesTitle; 
	GLabel lblStatusTitle; 
	GLabel lblStatus; 
	GCustomSlider sdrDelay; 
	GButton btnStart; 
	GButton btnStop; 
	GButton btnPause; 
	GButton btnResume; 
	GCheckbox cbxTrace; 
	GLabel lblDelayTitle; 
	GButton btnScript1; 
	GButton btnScript2; 
	GButton btnScript3;
}
